查看原文
其他

使用Android原生API绘制股票K线图

涛涛123759 郭霖 2019-04-29



今日科技快讯


近日,亚马逊CEO杰夫·贝索斯在推特上发布了一份夫妻二人的共同声明,宣布他和妻子麦肯齐已经离婚。“我们希望让人们意识到我们生活中出现新变化。正如我们的家人和密友们所知道的,经过长时间的感情探索和临时分居后,我们决定离婚,但将继续作为朋友共同生活。” 贝索斯目前是世界首富,身价高达1350亿美元,其妻子最多或可分得一半的财产。


作者简介


大家周一好,新的一周继续加油!

本篇来自 涛涛123759 的投稿文章,对Android中的原生股票图的绘制和讲解,希望对大家有所帮助。

涛涛123759 的博客地址:

https://www.jianshu.com/u/9e9a87c1909b


简介


K线图

K线又称阴阳线、棒线、红黑线或蜡烛线,起原于日本德川幕府时代(1603-1867)的米市交易,经过200多年的演进,形成了现在具有完整形式和分析理论的一种技术分析方法。下面先介绍以下柱状图表示的含义:

根据当日的开盘价,收盘价,最高价,最低价四项数据,可以绘制出以下的柱子:

阳 K 线–1 代表强升势,2 必需高开盘,3 回折不能超过阳 K 线的 35%。表示收盘价大于开盘价。

绿色阴K线-1代表强跌势,2 必须低开盘,3 回折不能超过阴K线的35%。表示收盘价小于开盘价。

十字星的形成表示强烈的市场,方向的移动或者方向的改变。表示收盘价等于开盘价。

更多K线图的走势讲解见

https://jingyan.baidu.com/article/47a29f241f9f3ec015239972.html

MA 移动平均线(均线)

移动平均线,英文名称为Moving Average,简称MA,原本意思是移动 平均。由于我们将其制作成线形,所以一般称为移动平均线,简称均线。

计算公式为:

MA ( 5 ) = ( C1+C2 +C3 +C4 +C5 ) /5

其中:Cn为第n日收盘价。例如C1,贝U为第1日收盘价。

  • 代码

/**
     * 计算ma
     *
     * @param datas
     */

    static void calculateMA(List<KLine> datas) {
        float ma5 = 0;
        float ma10 = 0;
        for (int i = 0; i < datas.size(); i++) {
            KLine point = datas.get(i);
            final float closePrice = point.getClosePrice();
            ma5 += closePrice;
            ma10 += closePrice;
            if (i >= 5) {
                ma5 -= datas.get(i - 5).getClosePrice();
                point.MA5Price = ma5 / 5f;
            } else {
                point.MA5Price = ma5 / (i + 1f);
            }
            if (i >= 10) {
                ma10 -= datas.get(i - 10).getClosePrice();
                point.MA10Price = ma10 / 10f;
            } else {
                point.MA10Price = ma10 / (i + 1f);
            }
        }
    }

BOLL布林线

BOLL指标是美国股市分析家约翰·布林根据统计学中的标准差原理设计出来的一种非常简单实用的技术分析指标。一般而言,股价的运动总是围绕某一价值中枢(如均线、成本线等)在一定的范围内变动,布林线指标正是在上述条件的基础上,引进了“股价信道”的概念,其认为股价信道的宽窄随着股价波动幅度的大小而变化,而且股价信道又具有变异性,它会随着股价的变化而自动调整。正是由于它具有灵活性、直观性和趋势性的特点,BOLL指标渐渐成为投资者广为应用的市场上热门指标。

在常态范围内,布林线使用的技术和方法

  1. 当股价穿越上限压力线时,卖点信号

  2. 当股价穿越下限支撑线时,买点信号

  3. 当股价由下向上穿越中界限时,为加码信号

  4. 当股价由上向下穿越中界线时,为卖出信号

  • 计算方法

中轨线=N日的移动平均线   

上轨线=中轨线+两倍的标准差   

下轨线=中轨线-两倍的标准差

  • BOLL指标的计算过程

计算MA

MA=N日内的收盘价之和÷N   

计算标准差MD  

MD=平方根(N-1)日的(C-MA)的两次方之和除以N  

计算MB、UP、DN线   

MB=(N-1)日的MA   

UP=MB+k×MD   

DN=MB-k×MD   

(K为参数,可根据股票的特性来做相应的调整,一般默认为2, c 为收盘价)

  • 代码

/**
     * 计算 BOLL 需要在计算ma之后进行
     *
     * @param datas
     */

    static void calculateBOLL(List<KLine> datas) {
        for (int i = 0; i < datas.size(); i++) {
            KLine point = datas.get(i);
            final float closePrice = point.getClosePrice();
            if (i == 0) {
                point.mb = closePrice;
                point.up = Float.NaN;
                point.dn = Float.NaN;
            } else {
                int n = 26;//20
                if (i < 26) {
                    n = i + 1;
                }
                float md = 0;
                for (int j = i - n + 1; j <= i; j++) {
                    float c = datas.get(j).getClosePrice();
                    float m = point.getMA26Price();
                    float value = c - m;
                    md += value * value;
                }
                md = md / (n - 1);
                md = (float) Math.sqrt(md);
                point.mb = point.getMA26Price();
                point.up = point.mb + 2f * md;
                point.dn = point.mb - 2f * md;
            }
            XLog.e("boll-mb:",point.mb+"");
        }

    }

KDJ是随机指标

随机指标KDJ一般是用于股票分析的统计体系,根据统计学原理,通过一个特定的周期(常为9日、9周等)内出现过的最高价、最低价及最后一个计算周期的收盘价及这三者之间的比例关系,来计算最后一个计算周期的未成熟随机值RSV,然后根据平滑移动平均线的方法来计算K值、D值与J值,并绘成曲线图来研判股票价格走势。

  • 计算方法

KDJ(9,3,3)

RSV=(收盘价-最近N个周期最低价)/(最近N个周期最高价-最近N个周期最低价)×100

k线(白线):RSV的m1个周期移动平均

D线(黄线):k值的m2个周期移动平均

J线(蓝线):3×D-2×K

  • 代码

/**
     * 计算kdj
     *
     * @param datas
     */

    static void calculateKDJ(List<KLine> datas) {
        float k = 0;
        float d = 0;

        for (int i = 0; i < datas.size(); i++) {
            KLine point = datas.get(i);
            final float closePrice = point.getClosePrice();
            int startIndex = i - 8;
            if (startIndex < 0) {
                startIndex = 0;
            }
            float max9 = Float.MIN_VALUE;
            float min9 = Float.MAX_VALUE;
            for (int index = startIndex; index <= i; index++) {
                max9 = Math.max(max9, datas.get(index).getHighPrice());
                min9 = Math.min(min9, datas.get(index).getLowPrice());
            }
            float rsv = 100f * (closePrice - min9) / (max9 - min9);
            if (i == 0) {
                k = rsv;
                d = rsv;
            } else {
                k = (rsv + 2f * k) / 3f;
                d = (k + 2f * d) / 3f;
            }
            point.k =Float.isNaN(k)?0:k;
            point.d = Float.isNaN(k)?0:d;
            float valueD=3f * k - 2 * d;
            point.j =Float.isNaN(valueD)?0:valueD;
        }

    }


正文


效果图

k线图

首先我i们定义一个基类ScrollAndScaleView,使其继承RelativeLayout使其可以在里面封装试图组;实现接口GestureDetector.OnGestureListener 和ScaleGestureDetector.OnScaleGestureListener实现缩放、滑动和点击事件。

  • 缩放事件

我们实现ScaleGestureDetector.OnScaleGestureListener接口中的onScale方法,从中控制最大缩放率mScaleXMax和最小缩放率mScaleXMin

@Override
    public boolean onScale(ScaleGestureDetector detector) {
        if (isClosePress) {
            if (!isScaleEnable()) {
                return false;
            }
            float oldScale = mScaleX;
            mScaleX *= detector.getScaleFactor();
            if (mScaleX < mScaleXMin) {
                mScaleX = mScaleXMin;
            } else if (mScaleX > mScaleXMax) {
                mScaleX = mScaleXMax;
            } else {
                onScaleChanged(mScaleX, oldScale);
            }
            if (mScaleX >= 2.0f || mScaleX <= 0.5f) {
                isScale = true;
            }
        }
        return true;
    }
  • 滑动事件

向左或向右滑动K线图是我们通过setScrollX(int scrollX)来确定我们需要滑动的位置mScrollX

public void setScrollX(int scrollX) {
        this.mScrollX = scrollX;
        scrollTo(scrollX, 0);
    }

获取需要滑动的位置mScrollX,然后调用scrollTo(int x, int y)来指定当前位置

@Override
    public void scrollTo(int x, int y) {
        if (isClosePress) {
            if (!isScrollEnable()) {
                mScroller.forceFinished(true);
                return;
            }
            int oldX = mScrollX;
            mScrollX = x;
            if (mScrollX < getMinScrollX()) {
                mScrollX = getMinScrollX();
                onRightSide();
                mScroller.forceFinished(true);
            } else if (mScrollX > getMaxScrollX()) {
                mScrollX = getMaxScrollX();
                onLeftSide();
                mScroller.forceFinished(true);
            }
            onScrollChanged(mScrollX, 0, oldX, 0);
            invalidate();
        }
    }
  • 手势冲突事件的处理

K线图的效果,当长按时会弹出对话框展示当前点对应的各指标的值;当单指左右滑动时试图跟着左右滑动;当双指进行操作时可控制放大缩小的缩放手势

我们通过变量isLongPress来控制长按手势,isClosePress表示是否关闭缩放手势。

protected boolean isLongPress = false;
protected boolean isClosePress = true//关闭长按时间

点击事件和长按事件可以在onTouchEvent() 处理:

@Override
   public boolean onTouchEvent(MotionEvent event) 
{
       switch (event.getAction() & MotionEvent.ACTION_MASK) {
           case MotionEvent.ACTION_DOWN:
               mClickTime = System.currentTimeMillis();
               break;
           case MotionEvent.ACTION_MOVE:
               //一个点的时候滑动
               if (event.getPointerCount() == 1) {
                   //长按之后移动
                   if (isLongPress || !isClosePress) {
                       calculateSelectedX(event.getX());
                       invalidate();
                   }
               }
               break;
           case MotionEvent.ACTION_UP:
               if (!isClosePress) {
                   isLongPress = false;
               }
               invalidate();
               break;
           case MotionEvent.ACTION_CANCEL:
               if (!isClosePress) {
                   isLongPress = false;
               }
               invalidate();
               break;
       }
       this.mDetector.onTouchEvent(event);
       this.mScaleDetector.onTouchEvent(event);
       return true;
   }

当长按时,会在onLongPress()方法中触发长安时间,此时我们把标识符onLongPress置true

@Override
public void onLongPress(MotionEvent e) {
    isLongPress = true;
    isClosePress = false;
}
  • 绘制视图水印

首先创建Bitmap对象:

private Bitmap mBitmapLogo = BitmapFactory.decodeResource(getContext().getResources(), R.drawable.ic_app_logo);

然后根据试图的位置来确定水印的位置,进行绘制

//主视图水印
 public void drawMainViewLogo(Canvas canvas) {
        if (mBitmapLogo != null) {
            int mLeft = getWidth() / 2 - mBitmapLogo.getWidth() / 2;
            if (!showBottomView) {
                mTopPadding = 0;
                maTextHeight=0;
            }
            int mTop = (mTopPadding + mMainHeight + maTextHeight) / 2 - mBitmapLogo.getHeight() / 2;
            canvas.drawBitmap(mBitmapLogo, mLeft, mTop, null);
        }
    }

//子试图水印
 public void drawChildViewLogo(Canvas canvas) {
        if (mBitmapLogo != null) {
            int mLeft = getWidth() / 2 - mBitmapLogo.getWidth() / 2;
            int mTop = mTopPadding + mMainHeight + mMainChildSpace + (mChildHeight / 2) - mBitmapLogo.getHeight() / 2;
            canvas.drawBitmap(mBitmapLogo, mLeft, mTop, null);
        }
    }
  • 绘制K线

坐标的转换, 绘制时我们需要当前点在屏幕中位置,当前点在坐标轴中位置和当前点translateX位置。

绘制步骤如下:

view中的x转化为TranslateX

public float xToTranslateX(float x) {
    return -mTranslateX + x / mScaleX;
}

translateX转化为view中的x

public float translateXtoX(float translateX) {
    return (translateX + mTranslateX) * mScaleX;
}

二分查找当前值的index

public int indexOfTranslateX(float translateX, int start, int end) {
        if (end == start) {
            return start;
        }
        if (end - start == 1) {
            float startValue = getX(start);
            float endValue = getX(end);
            return Math.abs(translateX - startValue) < Math.abs(translateX - endValue) ? start : end;
        }
        int mid = start + (end - start) / 2;
        float midValue = getX(mid);
        if (translateX < midValue) {
            return indexOfTranslateX(translateX, start, mid);
        } else if (translateX > midValue) {
            return indexOfTranslateX(translateX, midend);
        } else {
            return mid;
        }
    }

计算当前的显示区域位置和X、Y轴的单位长度

private void calculateValue() {
        if (!isLongPress()) {
            mSelectedIndex = -1;
        }
        mMainMaxValue = Float.MIN_VALUE;
        mMainMinValue = Float.MAX_VALUE;
        mChildMaxValue = Float.MIN_VALUE;
        mChildMinValue = Float.MAX_VALUE;

        mChildRightMaxValue = Float.MIN_VALUE;
        mChildRightMinValue = Float.MAX_VALUE;

        mStartIndex = indexOfTranslateX(xToTranslateX(0));
        mStopIndex = indexOfTranslateX(xToTranslateX(mWidth));
        for (int i = mStartIndex; i <= mStopIndex; i++) {
            IKLine point = (IKLine) getItem(i);
            if (mMainDraw != null) {
                mMainMaxValue = Float.parseFloat(formatValue(Math.max(mMainMaxValue, mMainDraw.getMaxValue(point))));
                mMainMinValue = Float.parseFloat(formatValue(Math.min(mMainMinValue, mMainDraw.getMinValue(point))));
            }
            if (mChildDraw != null) {
                mChildMaxValue = Float.parseFloat(formatValue(Math.max(mChildMaxValue, mChildDraw.getMaxValue(point))));
                mChildMinValue = Float.parseFloat(formatValue(Math.min(mChildMinValue, mChildDraw.getMinValue(point))));
                if (mShowChildRightYvalue) {//子视图右边Y轴最值
                    mChildRightMaxValue = Float.parseFloat(formatValue(Math.max(mChildRightMaxValue, mChildDraw.getRightMaxValue(point))));
                    mChildRightMinValue = Float.parseFloat(formatValue((Math.min(mChildRightMinValue, mChildDraw.getRightMinValue(point)))));
                }
            }
        }
        //最大值和最小值不相等时
        if (mMainMaxValue != mMainMinValue) {
            float padding = (mMainMaxValue - mMainMinValue) * 0.05f;
            mMainMaxValue += padding;
            mMainMinValue -= padding;
        } else {
            //当最大值和最小值都相等的时候 分别增大最大值和 减小最小值
            mMainMaxValue += Math.abs(mMainMaxValue * 0.05f);
            mMainMinValue -= Math.abs(mMainMinValue * 0.05f);
            if (mMainMaxValue == 0) {
                mMainMaxValue = 1;
            }
        }
        if (mChildMaxValue == mChildMinValue) {
            //当最大值和最小值都相等的时候 分别增大最大值和 减小最小值
            mChildMaxValue += Math.abs(mChildMaxValue * 0.05f);
            mChildMinValue -= Math.abs(mChildMinValue * 0.05f);
            if (mChildMaxValue == 0) {
                mChildMaxValue = 1;
            }
        }
        mMainScaleY = mMainRect.height() * 1f / (mMainMaxValue - mMainMinValue);
        mChildScaleY = mChildRect.height() * 1f / (mChildMaxValue - mChildMinValue);
        //右侧
        if (mShowChildRightYvalue) {
            if (mChildRightMaxValue == mChildRightMinValue) {
                //当最大值和最小值都相等的时候 分别增大最大值和 减小最小值
                mChildRightMaxValue += Math.abs(mChildRightMaxValue * 0.05f);
                mChildRightMinValue -= Math.abs(mChildRightMinValue * 0.05f);
                if (mChildRightMaxValue == 0) {
                    mChildRightMaxValue = 1;
                }
            }
            mChildRightScaleY = mChildRect.height() * 1f / (mChildRightMaxValue - mChildRightMinValue);
        }

        if (mAnimator.isRunning()) {
            float value = (float) mAnimator.getAnimatedValue();
            mStopIndex = mStartIndex + Math.round(value * (mStopIndex - mStartIndex));
        }
    }

绘制视图

private void drawK(Canvas canvas) {
        //保存之前的平移,缩放
        canvas.save();
        canvas.translate(mTranslateX * mScaleX, 0);
        canvas.scale(mScaleX, 1);

        mMaxValue = ((ICandle) getItem(mStartIndex)).getHighPrice();
        mMinValue = ((ICandle) getItem(mStartIndex)).getLowPrice();

        for (int i = mStartIndex; i <= mStopIndex; i++) {
            Object currentPoint = getItem(i);
            float currentPointX = getX(i);
            Object lastPoint = i == 0 ? currentPoint : getItem(i - 1);
            float lastX = i == 0 ? currentPointX : getX(i - 1);
            if (mMainDraw != null) {
                if (mMaxValue < ((ICandle) getItem(i)).getHighPrice()) {
                    mMaxValue = ((ICandle) getItem(i)).getHighPrice();
                    mMaxPoint = (ICandle) getItem(i);
                    mMaxX = currentPointX;
                } else if (mMinValue >= ((ICandle) getItem(i)).getLowPrice()) {
                    mMinValue = ((ICandle) getItem(i)).getLowPrice();
                    mMinPoint = (ICandle) getItem(i);
                    mMinX = currentPointX;
                }
                mMainDraw.drawTranslated(lastPoint, currentPoint, lastX, currentPointX, canvas, this, i);
            }
            if (mChildDraw != null) {
                mChildDraw.drawTranslated(lastPoint, currentPoint, lastX, currentPointX, canvas, this, i);
            }

        }
        if (mMainDraw != null && mMinPoint != null && mMaxPoint != null) {
            mMainDraw.drawMaxAndMin(this, canvas, mMaxX, mMinX, mMaxPoint, mMinPoint);
        }

        //画选择线
        if (isLongPress || !isClosePress) {
            IKLine point = (IKLine) getItem(mSelectedIndex);
            if (point == null) {
                return;
            }
            float x = getX(mSelectedIndex);
            float y = getMainY(point.getClosePrice());

            mSelectedLinePaint.setColor(ContextCompat.getColor(getContext(), R.color.chart_press_xian));//长按时线条显示文字的颜色
            canvas.drawLine(x, mMainRect.top, x, mChildRect.bottom, mSelectedLinePaint);
            canvas.drawLine(-mTranslateX, y, -mTranslateX + mWidth / mScaleX, y, mSelectedLinePaint);//隐藏横线
        }
        //还原 平移缩放
        canvas.restore();
    }

横竖屏切换处理

@Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {//横屏
            mMainDraw.setScreenStatus(false);
            paddingTopMA = mMainDraw.getLineFeed() ? DensityUtil.dp2px(30) : paddingTopBoll;
            Log.e("横屏:---------------""" + mMainDraw.getLineFeed() + paddingTopMA);
            setTopPadding(mShowMA ? paddingTopMA : paddingTopBoll);
        } else if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {//竖屏
            mMainDraw.setScreenStatus(true);
            paddingTopMA = DensityUtil.dp2px(30);
            Log.e("横屏:---------------""" + paddingTopMA);
            setTopPadding(mShowMA ? paddingTopMA : paddingTopBoll);
        }
        invalidate();
    }

关于K线和分时绘制过程大体上就是这些,代码还在进一步优化和完善。

欢迎大家提宝贵意见。下面是我免费开放的代码:

https://github.com/1067899750/kAndroid


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存